#
#  Change	Date		Author			Description
# --------------------------------------------------------------------------------------------------------
#   001	        07-12-2013	Adam Dosch		Updated /build POST call to include 'arch' option.
#                                                       Changing 'queue' document structure to align with the
#                                                       embedded layout of the 'build' document so it can be
#                                                       a simple re-insert without mangling a new json object.
#   002         07-14-2013      Adam Dosch              Made 'arch' a mandatory option for /BUILD rest call.
#                                                       Updated DB connection to use conntype init option#.
#   003         07-16-2013      Adam Dosch              Added default 'buildrelease' to /build REST call.
#                                                       Added 'processed' JSON option ('N': No, 'Y': yes) to
#                                                       both the default /build queue document and for /status
#                                                       find() query
#   006         08-04-2013      Adam Dosch              Changed 'processed' build/queue JSON document option
#                                                       (which signified if item was processed or picked up by
#                                                       a build daemon) to 'pickedUp' instead
#   007         10-08-2013      Adam Dosch              Adding 'build > frequency' to /build API --- defaulting to
#                                                       None.
#                                                       Disabling the ability to display '[build|config|ui]auth'
#                                                       configuration via API
#                                                       Setting 'pickedUp' state from N/Y to False/True to be more
#                                                       consistent with the overall document design
#

# REST CRUD rules
#
#  * To create a resource on the server, use POST.
#  * To retrieve a resource, use GET.
#  * To change the state of a resource or to update it, use PUT.
#  * To remove or delete a resource, use DELETE.

import platform
import os
import json
import ConfigParser
import datetime

import re

# Bottle 
try:
    import bottle
    from bottle import run, route, request, HTTPError, HTTPResponse, redirect, response, template, static_file
except ImportError:
    "Install bottle, please."

# PyMongo
try:
    from pymongo import errors as DatabaseErrors
    import pymongo
except ImportError:
    print "Install pymongo, please."

# RBS
try:
    from RBS import Config
    from system import Database
except ImportError:
    print "Update sys.path to load RBS modules, please."

configuration = None
dbconn = None

def doesPackageExist(packageName):
    pass

def check_auth(username, password):
    # Set user/pass check to None
    userauth = None
    passwordauth = None
    
    # Check requested URL and fetch authentication credentials for it
    if re.search("http://\S+/build\?*\S*", request.url):
        userauth = configuration.getConfiguration('buildauth')['username']
        passwordauth = configuration.getConfiguration('buildauth')['password']
    
    if re.search("http://\S+/query\?*\S*", request.url):
        userauth = configuration.getConfiguration('queueauth')['username']
        passwordauth = configuration.getConfiguration('queueauth')['password']

    if re.search("http://\S+/config\?*\S*", request.url):
        userauth = configuration.getConfiguration('configauth')['username']
        passwordauth = configuration.getConfiguration('configauth')['password']
    
    # Compare username/password
    if username == userauth and password == passwordauth:
        return True
    else:
        return False

def authenticate(message="You must authenticate before you can use, of course.", rc=40):
    # Set content type to JSON
    response.content_type = "application/json"
    
    # Set return message
    message = {'message': message, 'rc': rc}
    
    # Set HTTP response status and header
    response.status = "401 - Unauthorized"
    response.headers['WWW-Authenticate'] = 'Basic realm="RBS"'
    
    return json.dumps(message)

def requires_plain_auth(f):
    def decorated(*args, **kwargs):
        
        # Enable HTTP request auth via bottle auth() method        
        auth = request.auth
        
        # Validate authentication
        if not auth: 
            return authenticate()
        elif not check_auth(auth[0],auth[1]):
            response.status = "401 - Unauthorized"
            return authenticate("HTTP Authentication Failed.", 41)
        else:
            return f(*args, **kwargs)
    
    return decorated

def requires_hostbased_access(f):
    def check_host_access(*args, **kwargs):
        # Get remote host IP address
        remote_host = request.remote_addr
        
        # Default host access to wide-open (e.g. empty string) --- maybe not a good idea
        host_access_regex = ""
        
        # Get /build REST host access configuration
        if re.search("http://\S+/build\?*\S*", request.url):
            print "Matched REST /build"
            host_access_regex = configuration.getConfiguration('buildauth')['hostaccess']
    
        # Get /query REST host access configuration
        if re.search("http://\S+/query\?*\S*", request.url):
            print "Matched REST /query"
            host_access_regex = configuration.getConfiguration('queryauth')['hostaccess']
        
        # Get /config REST host access configuration
        if re.search("http://\S+/config/\S+/\S+", request.url):
            print "Matched REST /config"
            host_access_regex = configuration.getConfiguration('configauth')['hostaccess']
        
        print "Comparing host: ", remote_host
        print "Comparison: ", host_access_regex
        
        # If remote host doesn't match host access regex for particular REST call, return 'Forbidden'
        if not re.search(host_access_regex, remote_host):
            return HTTPError(403, "You don't have access to this REST service, juicebag.")
        else:
            return f(*args, **kwargs)
    
    return check_host_access

#================================
# favicon (GET)
#================================
@route('/favicon.ico', method='GET')
def get_favicon():
    return static_file('rbs.ico', root='./media/images')

#================================
# images (GET)
#================================
@route('/images/<image>', method='GET')
def get_image(image):
    return static_file(image, root='./media/images')

#================================
# Custom jQuery images (GET)
#================================
@route('/css/<location>/images/<image>', method='GET')
def get_custom_image(location, image):
    return static_file(image, root='./media/css/' + location + '/images/')

#================================
# CSS stylesheet (GET)
#================================
@route('/css/<location>/<css>', method='GET')
def get_css(location, css):
    return static_file(css, root='./media/css/' + location)

#================================
# Javascript stylesheet (GET)
#================================
@route('/js/<js>', method='GET')
def get_css(js):
    return static_file(js, root='./media/js/')

#================================
# Status REST read call (GET)
#================================

@route('/status')
@requires_hostbased_access
#@requires_plain_auth
def status():
    
    refreshrate = request.GET.get('refresh', default=120)
    
    try:
        # Connect to RBS 'queue' collection
        queue = dbconn.db['queue']
        
        # Return all queue items that have not been processed by a running build-daemon
        cursor = queue.find({'pickedUp': False}, {'_id': 0}).sort('build.submittime', pymongo.ASCENDING)
        
        queuelist = []
        
        for doc in cursor:
            print "doc: ", doc
            queuelist.append(doc)
        
    except Exception, e:
        return HTTPError(500, "Problem connecting to database, dude: %s" % e)
    
    # Put together status info
    statusInfo = {}
    
    (ostype, hostname, kernel, date, arch) = os.uname()
    
    statusInfo['OS'] = platform.platform()
    statusInfo['Kernel'] = platform.release()
    statusInfo['Hostname'] = platform.node()
    statusInfo['OS Architecture'] = platform.processor()
    statusInfo['Python Version'] = platform.python_version()

    #return "<html><head><meta http-equiv=refresh content=10></head><body><h2>%s</h2><p>Your address is: %s</br><p>Your builds are: %s</br></body></html>" % (configuration.getConfiguration('rbs')['title'], request['REMOTE_ADDR'], builds)
    return template('status', statusInfo=statusInfo, refreshrate=refreshrate, lastrefresh=datetime.datetime.now(), title=configuration.getConfiguration('rbs')['title'], queuelist=queuelist)

#================================
# Build REST write call (POST)
#================================

@route('/build', method='POST')
@requires_hostbased_access
@requires_plain_auth
def rpmbuild():
    """
    Method that supports RPM build submission requests.  Any mandatory options need to be defaulted to 'None',
    otherwise set to 'False'.
    """
    buildOptions = {}
    
    # Store HTTP GET arguments
    
    # Mandatory option
    buildOptions['package'] = request.GET.get('package', default=None)
    
    # Mandatory option
    buildOptions['ostype'] = request.GET.get('ostype', default=None)
    
    # Mandatory option
    buildOptions['arch'] = request.GET.get('arch', default=None)    

    # Optional API option - deploy
    buildOptions['deploy'] = request.GET.get('deploy', default=False)
    
    # Optional API option - buildrelease
    buildOptions['buildrelease'] = request.GET.get('buildrelease', default=False)
    
    # Optional API option - month (depends on buildrelease == True)
    buildOptions['month'] = request.GET.get('month', default=False)
    
    # Optional API option - year (depends on buildrelease == True)
    buildOptions['year'] = request.GET.get('year', default=False)
    
    # Optional API option - frequency
    buildOptions['frequency'] = request.GET.get('frequency', default=False)
    
    buildOptions['submittime'] = datetime.datetime.now()
    
    # Set JSON Content-type
    response.set_header('Content-Type', 'application/json')
    
    # If any build options are None, return '500'
    for buildopt, value in buildOptions.iteritems():
        #print buildopt, value
        
        if value == None:
            return json.dumps({'rc': 10, 'msg': 'Error, build service request was incomplete.  Missing required build option' })
 
    # If provided 'ostype' isn't defined in configuration, bail out
    if not buildOptions['ostype'] in configuration.getConfiguration('build')['ostypes']:
        return json.dumps({'rc': 11, 'msg': 'Invalid ostype provided in build request.  Check RBS configuration and reload it.' })
    
    # If provided 'arch' isn't defined in configuration, bail out
    if not buildOptions['arch'] in configuration.getConfiguration('build')['archtypes']:
        return json.dumps({'rc': 11, 'msg': 'Invalid archtype provided in build request.  Check RBS configuration and reload it.' })
    
    try:
        # Let's insert this into our queue
        queue = dbconn.db['queue']
        
        queue.insert({
            'package': buildOptions['package'],
            'ostype': buildOptions['ostype'],
            'arch': buildOptions['arch'],
            'pickedUp': False,
            'pickedUpTime': datetime.datetime(1970, 1, 1, 0, 0), #default to UNIX epoch
            'build': {
                'submittime': buildOptions['submittime'],
                'frequency': buildOptions['frequency']
            },
            'buildrelease': {
                'month': buildOptions['month'],
                'year': buildOptions['year']
            },
            'repo': {
                'deploy': buildOptions['deploy']
            }
        })
        
        return json.dumps({'rc': 0, 'msg': 'Build request for ' + buildOptions['package'] + ' successfully submitted to build service queue' })
    
    except DatabaseErrors.DuplicateKeyError, e:
        return json.dumps({'rc': 10, 'msg': 'Build request for package ' + buildOptions['package'] + ', ostype ' + buildOptions['ostype'] + ' and arch ' + buildOptions['arch'] + ' already exists in queue' })

    except DatabaseErrors.PyMongoError, e:
        return HTTPError(500, "Big problem summoning a build request to the queue, comrad: %s" % e)

#================================
# Build REST read call (GET)
#================================

@route('/query')
@requires_hostbased_access
@requires_plain_auth
def rpmquery():
    # Store HTTP GET options
    queryAction = request.GET.get('action', default=None)
    
    # Set JSON Content-type
    response.set_header('Content-Type', 'application/json')
    
    # Validate query action options
    if queryAction in ['buildstatus', 'info']:
        # /query?action=info&package=ledaps
        # /query?action=buildstatus&package=ledaps
        #
        packageName = request.GET.get('package', default=None)
    elif queryAction == "queue":
        # /query?action=queue&ostype=el6&num=5
        #
        osType = request.GET.get('ostype', default=None)
        numPkgsBuild = request.GET.get('num', default=None)
    else:
        return json.dumps({'rc': 20, 'msg': 'Error, query build service request action is not valid' })

#================================
# Config REST reload call (PUT)
#================================

@route('/config/reload', method='PUT')
#@requires_hostbased_access
#@requires_plain_auth
def config_reload():
    # Set JSON Content-type
    response.set_header('Content-Type', 'application/json')
    
    try:
        configuration.loadConfiguration()
        return json.dumps({'rc': 0, 'msg': 'Configuration reload successful, dude.'})
    except Exception, e:
        return HTTPError(500, "Darn! Had a problem reloading RBS configuration: %s" % e)

#================================
# Config REST read call (GET)
#================================

@route('/config/<section>/display', method='GET')
@requires_hostbased_access
@requires_plain_auth
def config_display(section=None):
    # Store HTTP GET options
    #section = request.GET.get('section', default=None)

    # Set JSON Content-type
    response.set_header('Content-Type', 'application/json')
    
    print configuration.parserConfig.sections()
    
    if section <> None:
        # Do not display/expose visibility of any authentication sections through API
        if not re.search("auth$", section):
            if section in configuration.parserConfig.sections():
                return json.dumps(configuration.getConfiguration(section))
            else:
                return HTTPError(404, "'%s' is not a valid config service option.  What were you thinking?" % section)
        else:
            return HTTPError(501, "'%s' is not a supported request to be displayed." % section)
    else:
        return HTTPError(404, "'%s' is not a valid config service option.  What were you thinking?" % section)

#================================
# Main section
#================================

def main():
    # Look for config in these locations
    for f in ['/home/adosch/code/python/remote-build-service','/tmp', '/etc/rbs', '/etc']:
        f += "/rbs.cfg"
        
        if os.path.isfile(f):
            if os.access(f, os.R_OK):
                
                global configuration
                configuration = Config(f)
                
                try:
                    configuration.loadConfiguration()
                except:
                    return HTTPError(500, "Aww, man!  Error trying to parse/load the configuration file.")
                
                break
    
    # If parserConfig is still None, then we didn't find one.  Get the hell out!
    if configuration == None:
        return HTTPError(500, "No configuration file to load!  You may want to do that before starting this service up, genius.")

    # Display traceback
    bottle.debug(True)
   
    # Create database connection
    try:
        global dbconn
        dbconn = Database.Connection("web", server=configuration.getConfiguration('rbs')['dbserver'])
        
    except DatabaseErrors.ConnectionFailure, e:
        return HTTPError(500, "Problem connecting to database, dude: %s" % e)
    
    # Start it up!
    run(host='192.168.20.7', port=8081, reloader=True)

if __name__ == '__main__':
    main()

